【小ネタ】Serverless Frameworkを利用してCloudFrontのIPレンジを自動でセキュリティグループに反映させる
はじめに
2016年12月1日追記
CloudFrontのリージョナルエッジキャッシュ導入により、CloudFrontで利用しているIPレンジが大幅に増えたようです。そのため、こちらのエントリで紹介しているスクリプトをそのまま実行してしまうとセキュリティグループのインバウンドルールデフォルト上限である50に引っかかってしまうようです。お使いになる際はご注意ください。ワークアラウンドとしては上限緩和申請などが考えられます。
こんにちは、中山です。
最近Serverless Frameworkを触り始めています。以前はLambdaのフレームワークとして主にApexを利用していましたが、最近噂を耳にしたので浮気気味です。どちらも素晴らしいフレームワークなのですが、Apexがシンプルさを重視しているのに対して、Serverless Frameworkの方はプラグイン機構などがあり、まさに「フレームワーク」といった感じで触っていて非常に面白いプロダクトだと思いました。
今回はServerless Frameworkのお勉強がてら、以前Apexで作成したCloudFrontのIPアドレスレンジを自動でセキュリティグループに反映するLambda関数を作成してみました。と言っても、Lambda関数そのものはawslabs/aws-cloudfront-samplesからの流用なのですが。。。Lambda関数ではなく「Serverless Frameworkの学習」ということで。。。
以前のエントリは以下を参照してください。
コード
いつも通りGitHubに置いておきました。ご自由にお使いください。
このコードは、CloudFrontなどのAWSリソースで利用しているIPレンジが書かれたJSONファイルに更新があった場合、それを特定のセキュリティグループに反映させるLambda関数をデプロイするものとなっています。
使い方
Serverless Frameworkの基本的な使い方は弊社のエントリにまとまっています。そちらを参照してください。
今回は上記リポジトリを利用する際に特有の内容について解説します。
1. コードのclone
リポジトリからコードをcloneしてください。注意点として、Lambda関数はサブモジュールで取り込んであります。そのため、 --recurse-submodule
を利用してcloneと同時にサブモジュールも取り込んでくる必要があります(もちろん後からでもできます)。
$ git clone [email protected]:knakayama/serverless-auto-update-cloudfront-ips.git --recurse-submodules Cloning into 'serverless-auto-update-cloudfront-ips'... remote: Counting objects: 10, done. remote: Compressing objects: 100% (6/6), done. remote: Total 10 (delta 0), reused 10 (delta 0), pack-reused 0 Receiving objects: 100% (10/10), done. Submodule 'function' (https://github.com/awslabs/aws-cloudfront-samples.git) registered for path 'function' Cloning into '****************************************************************************'... Submodule path 'function': checked out 'e49899a227c8b988db22cc01a658f2d761250080'
ディレクトリ構造は以下のようになっています。
$ tree serverless-auto-update-cloudfront-ips serverless-auto-update-cloudfront-ips ├── README.md ├── env │ └── dev ├── event.json ├── function │ ├── LICENSE │ ├── NOTICE.txt │ ├── README.md │ └── update_security_groups_lambda │ ├── README.md │ └── update_security_groups.py └── serverless.yml 4 directories, 8 files
2. 変数用ファイルの作成
serverless.yml
では custom
プロパティを利用して変数が書かれているファイルを読み込んでいます。この仕組みを利用することでAWSアカウントIDなど、あまりリポジトリへ直接保存しておきたくない情報を設置することが可能です。 serverless.yml
は以下のようになっています。
service: serverless-auto-update-cloudfront-ips provider: name: aws runtime: python2.7 stage: dev region: us-east-1 memorySize: 128 timeout: 10 iamRoleStatements: - Effect: Allow Action: - ec2:AuthorizeSecurityGroupIngress - ec2:RevokeSecurityGroupIngress Resource: Fn::Join: - "" - - "arn:aws:ec2:${self:provider.region}:" - ${self:custom.accountId} - ":security-group/*" - Effect: Allow Action: - ec2:DescribeSecurityGroups Resource: - "*" custom: ${file(env/${self:provider.stage}/secrets.yml)} functions: func: handler: function/update_security_groups_lambda/update_security_groups.lambda_handler # TODO: not work properly #events: # - sns: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged
変数用ファイルは以下のコマンドで作成可能です。
$ echo 'accountId: <_YOUR_AWS_ACCOUNT_ID_>' > env/dev/secrets.yml
Serverless Frameworkは柔軟な変数の利用が可能になっており、変数展開のネストなどもサポートされているます。詳細はドキュメントを参照してください。
3. Lambda関数のデプロイ
後はいつもどおり sls
コマンドでデプロイすればOKです。簡単ですね。
$ sls deploy -v Serverless: Packaging service… Serverless: Uploading CloudFormation file to S3… Serverless: Uploading service .zip file to S3… Serverless: Updating Stack… Serverless: Checking Stack update progress… CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - serverless-auto-update-cloudfront-ips-dev CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution CloudFormation - CREATE_COMPLETE - AWS::IAM::Role - IamRoleLambdaExecution CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Policy - IamPolicyLambdaExecution CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - FuncLambdaFunction CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Policy - IamPolicyLambdaExecution CloudFormation - CREATE_COMPLETE - AWS::IAM::Policy - IamPolicyLambdaExecution CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - FuncLambdaFunction CloudFormation - CREATE_COMPLETE - AWS::Lambda::Function - FuncLambdaFunction CloudFormation - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS - AWS::CloudFormation::Stack - serverless-auto-update-cloudfront-ips-dev CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - serverless-auto-update-cloudfront-ips-dev Serverless: Stack update finished… Service Information service: serverless-auto-update-cloudfront-ips stage: dev region: us-east-1 api keys: None endpoints: None functions: serverless-auto-update-cloudfront-ips-dev-func: arn:aws:lambda:us-east-1:************:function:serverless-auto-update-cloudfront-ips-dev-func Stack Outputs FuncLambdaFunctionArn: arn:aws:lambda:us-east-1:************:function:serverless-auto-update-cloudfront-ips-dev-func ServerlessDeploymentBucketName: serverless-auto-update-c-serverlessdeploymentbuck-************
デプロイ後のLambda関数は以下のコマンドで確認できます。
$ sls info -v Service Information service: serverless-auto-update-cloudfront-ips stage: dev region: us-east-1 api keys: None endpoints: None functions: serverless-auto-update-cloudfront-ips-dev-func: arn:aws:lambda:us-east-1:************:function:serverless-auto-update-cloudfront-ips-dev-func Stack Outputs FuncLambdaFunctionArn: arn:aws:lambda:us-east-1:************:function:serverless-auto-update-cloudfront-ips-dev-func ServerlessDeploymentBucketName: serverless-auto-update-c-serverlessdeploymentbuck-************
4. Lambda関数のトリガーとなるSNSトピックの設定
Serverless FrameworkでデプロイしたLambda関数は、 event
プロパティによってInvoke周りのさまざまな処理を簡単に記述可能です。これはApexにはないServerless Frameworkの大きな利点だと思います。しかし、今回このプロパティでは対応できないようです。Serverless Frameworkは内部的にCloudFormationを利用してAWSリソースを作成/アップデートしています。 event
プロパティも最終的にCloudFormationのテンプレートに変換され、スタックが作成されます(生成されたスタックは .serverless
ディレクトリ以下に設置されます)。
現時点(2016年11月14日)でCloudFormationは既存SNSトピックのサブスクリプションに未対応なため、すでに存在するSNSトピックからInvokeされる設定をこのプロパティでは定義できません。詳細はドキュメントを参照してください。しょうがないのでこの部分はマネジメントコンソールから設定します。もちろんawscliでもOKです。以下のようにLambdaのトリガーを設定してください。SNSトピックのARNは「arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged」です。
2016年11月20日追記
CloudFormationを利用した既存SNSトピックのサブスクリプションは2016年11月17日のアップデートで対応しています。
動作確認
Lambdaの準備が整ったので動作確認をしてみます。まずは、テスト用のセキュリティグループを作成します。このLambda関数は以下のタグが付いたものを対象にしてインバウンドルールをアップデートします。
- Name: cloudfront
- AutoUpdate: true
以下のコマンドを実行してテスト用セキュリティグループを作成してください。
$ export AWS_DEFAULT_REGION=us-east-1 $ aws ec2 create-tags \ --resources \ "$(aws ec2 create-security-group \ --group-name test \ --description test \ --vpc-id \ "$(aws ec2 describe-vpcs \ --query 'Vpcs[?IsDefault==`true`].VpcId' \ --output text)" \ --output text)" \ --tags Key=Name,Value=cloudfront Key=AutoUpdate,Value=true $ aws ec2 describe-security-groups \ --query 'SecurityGroups[?Tags[?Value==`cloudfront`]]' [ { "IpPermissionsEgress": [ { "IpProtocol": "-1", "IpRanges": [ { "CidrIp": "0.0.0.0/0" } ], "UserIdGroupPairs": [], "PrefixListIds": [] } ], "Description": "test", "Tags": [ { "Value": "cloudfront", "Key": "Name" }, { "Value": "true", "Key": "AutoUpdate" } ], "IpPermissions": [], "GroupName": "test", "VpcId": "vpc-********", "OwnerId": "************", "GroupId": "sg-c5b61eb8" } ]
続いて、Lambda関数をInvokeします。本当は実際に先のSNSトピックからパブリッシュしたいところですが、権限的にそれはできないので(当たり前ですが)トップディレクトリにある event.json
というサンプルデータを利用します。 invoke
サブコマンドを実行後、以下のように出力されたら成功です。
$ sls invoke --function func --path event.json --log [ "Updated sg-c5b61eb8", "Updated 1 of 1 SecurityGroups" ] -------------------------------------------------------------------- START RequestId: 8fc36dc2-a961-11e6-b5c0-4711afca47cf Version: $LATEST Received event: { "Records": [ { "EventVersion": "1.0", "EventSource": "aws:sns", "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", "Sns": { "SignatureVersion": "1", "Timestamp": "1970-01-01T00:00:00.000Z", "Signature": "EXAMPLE", "SigningCertUrl": "EXAMPLE", "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", "Message": "{\"create-time\": \"yyyy-mm-ddThh:mm:ss+00:00\", \"synctoken\": \"0123456789\", \"md5\": \"2d4d8d65073bf1ef395aafd91b38c2ca\", \"url\": \"https://ip-ranges.amazonaws.com/ip-ranges.json\"}", "Type": "Notification", "UnsubscribeUrl": "EXAMPLE", "TopicArn": "arn:aws:sns:EXAMPLE", "Subject": "TestInvoke" } } ] } Updating from https://ip-ranges.amazonaws.com/ip-ranges.json Found CLOUDFRONT range: 13.32.0.0/15 Found CLOUDFRONT range: 52.46.0.0/18 Found CLOUDFRONT range: 52.84.0.0/15 Found CLOUDFRONT range: 52.222.128.0/17 Found CLOUDFRONT range: 54.182.0.0/16 Found CLOUDFRONT range: 54.192.0.0/16 Found CLOUDFRONT range: 54.230.0.0/16 Found CLOUDFRONT range: 54.239.128.0/18 Found CLOUDFRONT range: 54.239.192.0/19 Found CLOUDFRONT range: 54.240.128.0/18 Found CLOUDFRONT range: 204.246.164.0/22 Found CLOUDFRONT range: 204.246.168.0/22 Found CLOUDFRONT range: 204.246.174.0/23 Found CLOUDFRONT range: 204.246.176.0/20 Found CLOUDFRONT range: 205.251.192.0/19 Found CLOUDFRONT range: 205.251.249.0/24 Found CLOUDFRONT range: 205.251.250.0/23 Found CLOUDFRONT range: 205.251.252.0/23 Found CLOUDFRONT range: 205.251.254.0/24 Found CLOUDFRONT range: 216.137.32.0/19 Found 1 SecurityGroups to update sg-c5b61eb8: Adding 13.32.0.0/15:80 sg-c5b61eb8: Adding 52.46.0.0/18:80 sg-c5b61eb8: Adding 52.84.0.0/15:80 sg-c5b61eb8: Adding 52.222.128.0/17:80 sg-c5b61eb8: Adding 54.182.0.0/16:80 sg-c5b61eb8: Adding 54.192.0.0/16:80 sg-c5b61eb8: Adding 54.230.0.0/16:80 sg-c5b61eb8: Adding 54.239.128.0/18:80 sg-c5b61eb8: Adding 54.239.192.0/19:80 sg-c5b61eb8: Adding 54.240.128.0/18:80 sg-c5b61eb8: Adding 204.246.164.0/22:80 sg-c5b61eb8: Adding 204.246.168.0/22:80 sg-c5b61eb8: Adding 204.246.174.0/23:80 sg-c5b61eb8: Adding 204.246.176.0/20:80 sg-c5b61eb8: Adding 205.251.192.0/19:80 sg-c5b61eb8: Adding 205.251.249.0/24:80 sg-c5b61eb8: Adding 205.251.250.0/23:80 sg-c5b61eb8: Adding 205.251.252.0/23:80 sg-c5b61eb8: Adding 205.251.254.0/24:80 sg-c5b61eb8: Adding 216.137.32.0/19:80 sg-c5b61eb8: Adding 13.32.0.0/15:443 sg-c5b61eb8: Adding 52.46.0.0/18:443 sg-c5b61eb8: Adding 52.84.0.0/15:443 sg-c5b61eb8: Adding 52.222.128.0/17:443 sg-c5b61eb8: Adding 54.182.0.0/16:443 sg-c5b61eb8: Adding 54.192.0.0/16:443 sg-c5b61eb8: Adding 54.230.0.0/16:443 sg-c5b61eb8: Adding 54.239.128.0/18:443 sg-c5b61eb8: Adding 54.239.192.0/19:443 sg-c5b61eb8: Adding 54.240.128.0/18:443 sg-c5b61eb8: Adding 204.246.164.0/22:443 sg-c5b61eb8: Adding 204.246.168.0/22:443 sg-c5b61eb8: Adding 204.246.174.0/23:443 sg-c5b61eb8: Adding 204.246.176.0/20:443 sg-c5b61eb8: Adding 205.251.192.0/19:443 sg-c5b61eb8: Adding 205.251.249.0/24:443 sg-c5b61eb8: Adding 205.251.250.0/23:443 sg-c5b61eb8: Adding 205.251.252.0/23:443 sg-c5b61eb8: Adding 205.251.254.0/24:443 sg-c5b61eb8: Adding 216.137.32.0/19:443 sg-c5b61eb8: Added 40, Revoked 0 END RequestId: 8fc36dc2-a961-11e6-b5c0-4711afca47cf REPORT RequestId: 8fc36dc2-a961-11e6-b5c0-4711afca47cf Duration: 2419.45 ms Billed Duration: 2500 ms Memory Size: 128 MB Max Memory Used: 60 MB
先程作成したセキュリティグループを見てみると、インバウンドルールが更新されていることを確認できます。
$ aws ec2 describe-security-groups \ --query 'SecurityGroups[?Tags[?Value==`cloudfront`]]' [ { "IpPermissionsEgress": [ { "IpProtocol": "-1", "IpRanges": [ { "CidrIp": "0.0.0.0/0" } ], "UserIdGroupPairs": [], "PrefixListIds": [] } ], "Description": "test", "Tags": [ { "Value": "cloudfront", "Key": "Name" }, { "Value": "true", "Key": "AutoUpdate" } ], "IpPermissions": [ { "PrefixListIds": [], "FromPort": 80, "IpRanges": [ { "CidrIp": "13.32.0.0/15" }, { "CidrIp": "204.246.164.0/22" }, { "CidrIp": "204.246.168.0/22" }, { "CidrIp": "204.246.174.0/23" }, { "CidrIp": "204.246.176.0/20" }, { "CidrIp": "205.251.192.0/19" }, { "CidrIp": "205.251.249.0/24" }, { "CidrIp": "205.251.250.0/23" }, { "CidrIp": "205.251.252.0/23" }, { "CidrIp": "205.251.254.0/24" }, { "CidrIp": "216.137.32.0/19" }, { "CidrIp": "52.222.128.0/17" }, { "CidrIp": "52.46.0.0/18" }, { "CidrIp": "52.84.0.0/15" }, { "CidrIp": "54.182.0.0/16" }, { "CidrIp": "54.192.0.0/16" }, { "CidrIp": "54.230.0.0/16" }, { "CidrIp": "54.239.128.0/18" }, { "CidrIp": "54.239.192.0/19" }, { "CidrIp": "54.240.128.0/18" } ], "ToPort": 80, "IpProtocol": "tcp", "UserIdGroupPairs": [] }, { "PrefixListIds": [], "FromPort": 443, "IpRanges": [ { "CidrIp": "13.32.0.0/15" }, { "CidrIp": "204.246.164.0/22" }, { "CidrIp": "204.246.168.0/22" }, { "CidrIp": "204.246.174.0/23" }, { "CidrIp": "204.246.176.0/20" }, { "CidrIp": "205.251.192.0/19" }, { "CidrIp": "205.251.249.0/24" }, { "CidrIp": "205.251.250.0/23" }, { "CidrIp": "205.251.252.0/23" }, { "CidrIp": "205.251.254.0/24" }, { "CidrIp": "216.137.32.0/19" }, { "CidrIp": "52.222.128.0/17" }, { "CidrIp": "52.46.0.0/18" }, { "CidrIp": "52.84.0.0/15" }, { "CidrIp": "54.182.0.0/16" }, { "CidrIp": "54.192.0.0/16" }, { "CidrIp": "54.230.0.0/16" }, { "CidrIp": "54.239.128.0/18" }, { "CidrIp": "54.239.192.0/19" }, { "CidrIp": "54.240.128.0/18" } ], "ToPort": 443, "IpProtocol": "tcp", "UserIdGroupPairs": [] } ], "GroupName": "test", "VpcId": "vpc-********", "OwnerId": "************", "GroupId": "sg-c5b61eb8" } ]
まとめ
いかがだったでしょうか。
まだ、Serverless Frameworkを触り始めてから日が経っていないですが、CloudFormationやLambda関数など使い慣れたサービスを内部的に利用しているため、すんなりと入門できました。今後はプラグイン機構などより高度なトピックをご紹介できればと思います。
本エントリがみなさんの参考になれば幸いです。